Clojureでリバーシを作ってみる - Part2 Clojureで盤面と石をどう表現するか
#Clojureでリバーシを作ってみる
プロジェクトが用意できたので、次はリバーシの基本的なパーツである盤面と石を表現してみます。簡単なゴールとして以下のような出力ができることを目指します。
code:shell
+-------------------------------+
| | | | | | | | |
|---+---+---+---+---+---+---+---|
| | | | | | | | |
|---+---+---+---+---+---+---+---|
| | | | | | | | |
|---+---+---+---+---+---+---+---|
| | | | ○ | ● | | | |
|---+---+---+---+---+---+---+---|
| | | | ● | ○ | | | |
|---+---+---+---+---+---+---+---|
| | | | | | | | |
|---+---+---+---+---+---+---+---|
| | | | | | | | |
|---+---+---+---+---+---+---+---|
| | | | | | | | |
+-------------------------------+
リバーシの盤面は8x8の計64マスとなっているので、これは単純な二次元配列のようなデータ構造で表現できそうだと気付きます。
code:clojure
[nil nil nil nil nil nil nil nil
nil nil nil nil nil nil nil nil
nil nil nil nil nil nil nil nil
nil nil nil "○" "●" nil nil nil
nil nil nil "●" "○" nil nil nil
nil nil nil nil nil nil nil nil
nil nil nil nil nil nil nil nil
nil nil nil nil nil nil nil nil]
WIP
この考えをそのままコードに落とし込むと以下のようになります。
code:clojure
reversi.core> (require 'clojure.pprint :as p)
;;=> nil
reversi.core> (p/pprint (repeat 8 (repeat 8 :x)))
((:x :x :x :x :x :x :x :x)
(:x :x :x :x :x :x :x :x)
(:x :x :x :x :x :x :x :x)
(:x :x :x :x :x :x :x :x)
(:x :x :x :x :x :x :x :x)
(:x :x :x :x :x :x :x :x)
(:x :x :x :x :x :x :x :x)
(:x :x :x :x :x :x :x :x))
;;=> nil
このようなデータ構造でマス目すべての状態を管理するのもいいかもしれませんが、ここでは置かれた石だけを表現するようなデータを管理してみようと思います。次のようなマップデータとして置かれた石を管理してみます。
code:clojure
{3 3 :w 4 3 :b
3 4 :b 4 4 :w}
なんとなく意図が分かると思います。盤面の左上を0とする横軸をx軸、同様に左上を0とする縦軸をy軸として、[x y]という形で石が置かれた場所を示します。そして置かれた石を黒なら:b、白なら:wと表現しました。これで[3 3]には白が置かれていることが分かります。リバーシの盤面でいうと、次の図のように配置した石を表現しています(x軸をA~H、y軸を1~8で表現しているので読み替えが必要です)。
https://gyazo.com/55949dfb0228df091fd175e160682862
図1: 初期の盤面
それでは早速reversi.storeというネームスペースを作って、置かれた石を管理していきます。
code:src/reversi/store.clj
(ns reversi.store)
(def initial-stones
{3 3 :w 4 3 :b
3 4 :b 4 4 :w})
(defonce stones
(atom initial-stones))
(defn reset-stones! []
(reset! stones initial-stones))
initial-stonesはリバーシで最初に置かなければいけない石の状態です。実際に石を置いていくのはstonesの方です。これはAtomを使っているので、swap!やreset!で更新することが可能です。また不慮のリロードや再評価などでstonesが上書きされては困るので、defonceでstonesを再評価してリセットできないようにしています。その代わり、reset-stones!という関数を明示的に呼び出せば、stonesを初期状態にすることができるようになっています。
リバーシのおいて石を「取り除く」という表現はないので、「置く」あるいは「ひっくり返す」が表現できればいいわけですが、どちらも次のように表現することができます。
code:clojure
reversi.core> (require 'reversi.store :as store)
;;=> nil
reversi.core> (swap! store/stones assoc 4 5 :b)
;;=> {3 3 :w, 4 3 :b, 3 4 :b, 4 4 :w, 4 5 :b}
reversi.core> (swap! store/stones assoc 4 4 :b)
;;=> {3 3 :w, 4 3 :b, 3 4 :b, 4 4 :b, 4 5 :b}
あるいは一度に置く石とひっくり返す石を指定することもできます。
code:clojure
reversi.core> (swap! store/stones assoc 5 3 :w 4 3 :w)
;;=> {3 3 :w, 4 3 :w, 3 4 :b, 4 4 :b, 4 5 :b, 5 3 :w}
これで置かれた石は表現できたました。今度は盤面を描いていきます。盤面を描くには8x8マスを用意しないといけないわけですが、あわせて必要なのは石を置く座標です。なんとなくそれっぽいものを簡単に用意してみましょう。
code:clojure
reversi.core> (p/pprint
(for y (range 8)
(for x (range 8)
x y)))
((0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0)
(0 1 1 1 2 1 3 1 4 1 5 1 6 1 7 1)
(0 2 1 2 2 2 3 2 4 2 5 2 6 2 7 2)
(0 3 1 3 2 3 3 3 4 3 5 3 6 3 7 3)
(0 4 1 4 2 4 3 4 4 4 5 4 6 4 7 4)
(0 5 1 5 2 5 3 5 4 5 5 5 6 5 7 5)
(0 6 1 6 2 6 3 6 4 6 5 6 6 6 7 6)
(0 7 1 7 2 7 3 7 4 7 5 7 6 7 7 7))
;;=> nil
ちょうど左上から[0 0]、右下が[7 7]になるようなデータを手に入れることができました。あとはこの座標データを元に石を表現してみればよさそうです。その座標に置いてある石が黒か白かは(get @store/stones [3 3])のようにすれば簡単に取得することができます。:bや:w、あるいはnilといった情報から文字列表現に変換する関数は次のように定義できます。
code:clojure
reversi.core> (defn render-stone stone
(case stone
:b "●"
:w "○"
" "))
;;=> #'reversi.core/render-stone
reversi.core> (render-stone :b)
;;=> "●"
reversi.core> (render-stone :w)
;;=> "○"
reversi.core> (render-stone nil)
;;=> " "
そのままですが、:bのとき黒丸、:wのとき白丸、それ以外なら半角スペースにするような関数です。これらを踏まえると、すべての座標データを文字列表現に変換するのは以下のように書けます。
code:clojure
reversi.core> (p/pprint
(for y (range 8)
(for x (range 8)
(render-stone (get @store/stones x y)))))
((" " " " " " " " " " " " " " " ")
(" " " " " " " " " " " " " " " ")
(" " " " " " " " " " " " " " " ")
(" " " " " " "○" "●" " " " " " ")
(" " " " " " "●" "○" " " " " " ")
(" " " " " " " " " " " " " " " ")
(" " " " " " " " " " " " " " " ")
(" " " " " " " " " " " " " " " "))
;;=> nil
これで盤面の各マスに対応する状況が分かりやすく見えるようになりました。あとはこれを元に肉付けしていって、reversi.viewというネームスペースを作ってみます。
code:src/reversi/view.clj
(ns reversi.view
(:require clojure.string :as str))
(def header
(str " "
(->> (range (int \A) (inc (int \H)))
(map #(format " %s " (char %)))
(str/join "|"))))
(def outline
(format " +%s+" (str/join "-" (repeat 8 "---"))))
(def line
(format "---+%s+" (str/join "+" (repeat 8 "---"))))
(defn- render-stone stone
(case stone
:b "●"
:w "○"
" "))
(defn render-board stones
(str
header \newline
outline \newline
(str/join
(str \newline line \newline)
(for y (range 8)
(format " %s | %s |"
(inc y)
(str/join
" | "
(for x (range 8)
(-> (get stones x y)
render-stone))))))
\newline outline))
render-boardという関数を作りました。これは引数として置かれた石の情報を受け取って盤面の文字列表現を返します。早速REPLで試してみると以下のように表示がされました。
code:clojure
reversi.core> (require 'reversi.view :as view)
;;=> nil
reversi.core> (println (view/render-board @store/stones))
A | B | C | D | E | F | G | H
+-------------------------------+
1 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
2 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
3 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
4 | | | | ○ | ● | | | |
---+---+---+---+---+---+---+---+---+
5 | | | | ● | ○ | | | |
---+---+---+---+---+---+---+---+---+
6 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
7 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
8 | | | | | | | | |
+-------------------------------+
;;=> nil
ここまでできると簡単ですが、リバーシとして遊ぶことができます。
code:clojure
reversi.core> (swap! store/stones assoc 4 5 :b 4 4 :b)
;;=> {3 3 :w, 4 3 :b, 3 4 :b, 4 4 :b, 4 5 :b}
reversi.core> (println (view/render-board @store/stones))
A | B | C | D | E | F | G | H
+-------------------------------+
1 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
2 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
3 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
4 | | | | ○ | ● | | | |
---+---+---+---+---+---+---+---+---+
5 | | | | ● | ● | | | |
---+---+---+---+---+---+---+---+---+
6 | | | | | ● | | | |
---+---+---+---+---+---+---+---+---+
7 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
8 | | | | | | | | |
+-------------------------------+
;;=> nil
しかし、あまりにも不格好というか、これでリバーシのプログラムというには少々お粗末なので、次ではもう少し改良していきたいと思います。
Clojureでリバーシを作ってみる - Part1 プロジェクトを用意する <- | -> Clojureでリバーシを作ってみる - Part3 周りの石をひっくり返せるようにしてみる